﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Caching;
using MetaCollection.Framework.Change;
using MetaCollection.Framework.Model;

namespace MetaCollection.Framework.Repository
{


	public class CachingItemRepository : IItemRepository
	{
		private IItemRepository m_repo;
		private const string BASEKEY = "MC";

		# region Caching Methods

		/// <summary>
		/// Gets the cache.
		/// </summary>
		/// <value>The cache.</value>
		private static Cache Cache
		{
			get
			{
				return (System.Web.HttpContext.Current == null) ? System.Web.HttpRuntime.Cache : System.Web.HttpContext.Current.Cache;
			}
		}

		/// <summary>
		/// Returns a Cache key for an item reference.
		/// </summary>
		/// <param name="id">The id (of the item).</param>
		/// <returns>A cache key for an item reference.</returns>
		private static string ReferenceKey(Guid id)
		{
			return String.Format(CultureInfo.InvariantCulture, "CacheReference:{0}", id);
		}

		/// <summary>
		/// Determines whether the specified key has cache.
		/// </summary>
		/// <param name="key">The key.</param>
		/// <param name="value">The value.</param>
		/// <returns>
		/// 	<c>true</c> if the specified key has cache; otherwise, <c>false</c>.
		/// </returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		static bool HasCache(string key, out IEnumerable<IItemInstance> value)
		{
			try
			{
				if (Cache[key] == null)
				{
					value = null;
					return false;
				}
				value = (IEnumerable<IItemInstance>)Cache[key];
			}
			catch (InvalidCastException)
			{
				value = null;
				return false;
			}
			return true;
		}

		/// <summary>
		/// Determines whether the specified key has cache.
		/// </summary>
		/// <param name="key">The key.</param>
		/// <param name="value">The value.</param>
		/// <returns>
		/// 	<c>true</c> if the specified key has cache; otherwise, <c>false</c>.
		/// </returns>
		static bool HasCache(string key, out IItemInstance value)
		{
			try
			{
				if (Cache[key] == null)
				{
					value = null;
					return false;
				}
				value = (IItemInstance)Cache[key];
			}
			catch (InvalidCastException)
			{
				value = null;
				return false;
			}
			return true;
		}

		/// <summary>
		/// Caches the copy.
		/// </summary>
		/// <param name="items">The items.</param>
		/// <param name="setId">The id of the parent item to create a cache dependency for this set on.</param>
		/// <param name="key">The key.</param>
		/// <returns></returns>
		static IEnumerable<IItemInstance> CacheCopy(IEnumerable<IItemInstance> items, Guid setId, string key)
		{
			bool iterationComplete = false;
			List<ItemInstance> cacheItems = new List<ItemInstance>();
			AggregateCacheDependency dependencies = new AggregateCacheDependency();
			foreach (IItemInstance item in items)
			{
				iterationComplete = false;
				yield return item;
				cacheItems.Add(item.ToStruct()); // Convert to value type
				//Insert the item id into cache for dependency purposes.
				if (Cache[ReferenceKey(item.Id)] == null)
				{
					Cache.Insert(ReferenceKey(item.Id), DateTime.Now.Ticks, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null);
				}
				CacheDependency dependency = new CacheDependency(null, new string[] { ReferenceKey(item.Id) });
				dependencies.Add(dependency);
				iterationComplete = true;
			}
			if (iterationComplete)
			{
				using (CacheDependency dependency = new CacheDependency(null, new string[] { ReferenceKey(setId) }))
				{
					dependencies.Add(dependency);
					Cache.Insert(key, cacheItems, dependencies, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.BelowNormal, null);
				}
			}
		}

		/// <summary>
		/// Caches the copy.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <param name="key">The key.</param>
		/// <returns></returns>
		static IItemInstance CacheCopy(IItemInstance item, string key)
		{
			AggregateCacheDependency dependencies = new AggregateCacheDependency();
			if (Cache[ReferenceKey(item.Id)] == null)
			{
				Cache.Insert(ReferenceKey(item.Id), DateTime.Now.Ticks, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null);
			}
			using (CacheDependency dependency = new CacheDependency(null, new string[] { ReferenceKey(item.Id) }))
			{
				dependencies.Add(dependency);
				IItemInstance itemStruct = item.ToStruct();
				Cache.Insert(key, itemStruct, dependencies, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.BelowNormal, null);
				return itemStruct;
			}
		}

		/// <summary>
		/// Removes the cache copy.
		/// </summary>
		/// <param name="key">The key.</param>
		static void CacheRemove(string key)
		{
			Cache.Remove(key);
		}

		# endregion

		#region IItemInstance Methods

		public void Initialize(string configData)
		{

		}

		public CachingItemRepository(IItemRepository baseRepository)
		{
			m_repo = baseRepository;
		}

		/// <summary>
		/// Gets all versions of an item and caches each item in the list, along with the dependency items.
		/// </summary>
		/// <param name="id">The id of the item.</param>
		/// <param name="opt">Load Options for what data to return</param>
		/// <returns>
		/// An ItemInstance for the given Item Id or null if no object found
		/// </returns>
		public IEnumerable<IItemInstance> GetItem(Guid id, LoadOptions opt)
		{
			string key = string.Format("{0}:GetItem_{1}_{2}", BASEKEY, id, opt);
			IEnumerable<IItemInstance> items;
			if (!HasCache(key, out items))
			{
				items = CacheCopy(m_repo.GetItem(id, opt), id, key);
			}
			foreach (IItemInstance item in items)
			{
				yield return item;
			}
		}

		/// <summary>
		/// Finds an Item by Id-Version and caches a dependency item.
		/// </summary>
		/// <param name="id">The id.</param>
		/// <param name="version">The version.</param>
		/// <param name="opt">Load Options for what data to return</param>
		/// <returns>
		/// An ItemInstance for the given Item Id-Version or null if no object found
		/// </returns>
		public IItemInstance GetItem(Guid id, int version, LoadOptions opt)
		{
			string key = string.Format(CultureInfo.CurrentCulture, "{0}:GetItem_{1}_{2}_{3}", BASEKEY, id, version, opt);
			IItemInstance item;
			if (!HasCache(key, out item))
			{
				item = CacheCopy(m_repo.GetItem(id, version, opt), key);
			}
			return item;
		}

		/// <summary>
		/// Finds an item by name with a given parent and caches each item in the list, along with the dependency items.
		/// </summary>
		/// <param name="parentId">The parent id.</param>
		/// <param name="name">The name.</param>
		/// <param name="opt">Load Options for what data to return</param>
		/// <returns>an ItemInstance</returns>
		/// <remarks>Returns the published version ?? max(version) of each item</remarks>
		public IEnumerable<IItemInstance> FindByName(Guid parentId, string name, LoadOptions opt)
		{
			string key = string.Format("{0}:FindByName_{1}_{2}_{3}", BASEKEY, parentId, name, opt);
			IEnumerable<IItemInstance> items;
			if (!HasCache(key, out items))
			{
				items = CacheCopy(m_repo.FindByName(parentId, name, opt), parentId, key);
			}
			foreach (IItemInstance item in items)
			{
				yield return item;
			}			
		}

		/// <summary>
		/// Finds all the items with a given parent and caches each item in the list, along with the dependency items.
		/// </summary>
		/// <param name="parentId">The parent id.</param>
		/// <param name="includeIndirect">if set to <c>true</c> the children that are not directly under the parent are also included.</param>
		/// <param name="opt">Load Options for what data to return</param>
		/// <returns></returns>
		/// <remarks>Returns the published version ?? max(version) of each item</remarks>
		public IEnumerable<IItemInstance> FindByParent(Guid parentId, bool includeIndirect, LoadOptions opt)
		{
			string key = string.Format("{0}:FindByParent_{1}_{2}", BASEKEY, parentId, opt);
			IEnumerable<IItemInstance> items;
			if (!HasCache(key, out items))
			{
				items = CacheCopy(m_repo.FindByParent(parentId, includeIndirect, opt), parentId, key);
			}
			foreach (IItemInstance item in items)
			{
				yield return item;
			}
		}

		/// <summary>
		/// Puts the item into persistant storage or updates an existing item and removes the item from the cache, along with previous versions of the item.
		/// </summary>
		/// <param name="item"></param>
		/// <param name="changeCollection"></param>
		/// <param name="requestType"></param>
		/// <returns></returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
		public IItemInstance SaveItem(IItemInstance item, ItemChangeCollection changeCollection) //, RequestTypes requestType)
		{
			if (item == null)
				throw new ArgumentNullException("item", "Item can not be null.");

			if (changeCollection == null)
				throw new ArgumentNullException("changeCollection", "The change collection can not be null.");

			if (changeCollection.Changes.Any(p => p.ChangeType.HasType(ItemChangeTypes.Create)))
				CacheRemove(ReferenceKey(item.ParentId));

			CacheRemove(ReferenceKey(item.Id));
			return m_repo.SaveItem(item, changeCollection);
		}

		/// <summary>
		/// Deletes the item.
		/// </summary>
		/// <param name="id">The id.</param>
		public void RemoveItem(Guid id)
		{
			CacheRemove(ReferenceKey(id));
			m_repo.RemoveItem(id);
		}

		/// <summary>
		/// Deletes the item.
		/// </summary>
		/// <param name="id">The id.</param>
		/// <param name="version">The version.</param>
		public void RemoveItem(Guid id, int version)
		{
			CacheRemove(ReferenceKey(id));
			m_repo.RemoveItem(id, version);
		}
		#endregion

	}
}
